from __future__ import with_statement

import sys
import wsgiref.util
try:
    import cStringIO as StringIO
except ImportError:
    import StringIO
import jsonlib
import traceback
import time
import uuid
import threading
import kineta.filedb as filedb
import kineta.utils as utils
from kineta.error import KinetaError


class Server(utils.DeleteHelper):
    """
    A WSGI application which hosts Kineta databases and makes them available
    via HTTP.
    """

    def _pre_init(self):
        self.databases = None

    def __init__(self, dbpaths):
        """
        Initializes the application by loading the specified databases.

        @type dbpaths: dict
        @param dbpaths: A dictionary containing names and databases to host.
                       The keys of the dict should be ASCII
                       strings which are used as segments in the server URL.
                       The values are FileDB instances, which will be hosted
                       at those URL paths.  As long as the L{close} method is
                       called on the Server object, the database instances will
                       be closed as well, so their lifetime does not need to
                       be managed by the caller.

                       For example, if the application was being hosted at
                       http://localhost:7000/, a parameter of::

                            { "mydbname": FileDB("/home/mydb1", "300MB") }

                       would open the database located at /home/mydb1 with a
                       300MB cache, and make it available at the URL
                       http://localhost:7000/mydbname.
        """
        self._txns = {}
        self._readers = {}
        self._logbuffer = []
        self.databases = {}
        for name, db in dbpaths.iteritems():
            self.databases[name] = db
            self._txns[name] = {}
            self._readers[name] = {}
            db.set_bg_error_cb(lambda err: self._log_error(name, err))
        self._lock = threading.Lock()

        self._errheads = [('Content-type', 'text/plain')]
        self._jsonheads = [('Content-type', 'application/json; charset=utf-8')]
        self._badmethod = 'Method not allowed'
        self._notfound = 'Database not found'
        self._badcmd = 'Bad command'
        self._parseerr = 'JSON parse error: %s'
        self._internal = 'Internal error'
        self._cmds = {'begin': Server._do_begin,
                      'commit': Server._do_commit_rollback,
                      'rollback': Server._do_commit_rollback,
                      'select': Server._do_select,
                      'select_next': Server._do_selectnext,
                      'select_close': Server._do_selectclose,
                      'explain': Server._do_cmd,
                      'insert': Server._do_cmd,
                      'insert_bulk': Server._do_cmd,
                      'update': Server._do_cmd,
                      'delete': Server._do_cmd,
                      'list_tables': Server._do_cmd,
                      'list_fields': Server._do_cmd,
                      'list_indexes': Server._do_cmd,
                      'create_index': Server._do_cmd,
                      'drop_index': Server._do_cmd,
                      'drop_table': Server._do_cmd,
                      'wait_for_bg_tasks': Server._do_cmd}

    def _jsonheaders(self, outp):
        return self._jsonheads + [('Content-Length', str(len(outp)))]

    def _errheaders(self, err):
        return self._errheads + [('Content-Length', str(len(err)))]

    def __del__(self):
        self.close()

    def close(self):
        """
        Closes all databases hosted by the server.
        """
        if self.databases is not None:
            for db in self.databases.itervalues():
                db.close()
            self.databases = None

    def __call__(self, environ, start_response):
        self._obj_timeout()

        self._log_flush(environ['wsgi.errors'])

        if environ['REQUEST_METHOD'] != 'POST':
            start_response('405 Method Not Allowed',
                           self._errheaders(self._badmethod))
            return [self._badmethod]

        dbname = wsgiref.util.shift_path_info(environ)
        if dbname is None or dbname not in self.databases:
            start_response('404 Not Found', self._errheaders(self._notfound))
            return [self._notfound]

        cmd = wsgiref.util.shift_path_info(environ)
        if cmd not in self._cmds:
            start_response('400 Bad Request', self._errheaders(self._badcmd))
            return [self._badcmd]

        leftover = environ['PATH_INFO']
        if leftover != '/' and leftover != '':
            start_response('400 Bad Request', self._errheaders(self._badcmd))
            return [self._badcmd]

        try:
            val = ""
            if 'CONTENT_LENGTH' in environ:
                val = environ['wsgi.input'].read(
                                                int(environ['CONTENT_LENGTH']))
            if val == "": #pragma: no cover
                json = {}
            else:
                json = jsonlib.read(val)
                if not isinstance(json, dict):
                    er = self._parseerr % "Top-level not an object"
                    start_response('400 Bad Request', self._errheaders(er))
                    return [er]
        except jsonlib.ReadError, e:
            er = self._parseerr % e
            start_response('400 Bad Request', self._errheaders(er))
            return [er]
        try:
            parms, ids = self._get_params(dbname, json)
            outp = self._cmds[cmd](self, dbname, self.databases[dbname], cmd,
                                   parms, ids)
            start_response('200 OK', self._jsonheaders(outp))
            return [outp]
        except:
            environ['wsgi.errors'].write(traceback.format_exc())
            er = str(sys.exc_info()[1])
            start_response('500 Internal Server Error', self._errheaders(er))
            return [er]

    def _log_error(self, name, err):
        with self._lock:
            self._logbuffer.append("BG error in database %s: %s" % (name, err))

    def _log_flush(self, stream):
        with self._lock:
            if len(self._logbuffer) > 0:
                stream.writelines(self._logbuffer)
                self._logbuffer = []

    def _get_params(self, dbname, json):
        d = utils.strkeys(json)
        ids = {}
        with self._lock:
            for store, obj, name in ((self._txns, "txn", "transaction"),
                                      (self._readers, "next", "reader")):
                if obj in d and d[obj] is not None:
                    id = d[obj]
                    if id not in store[dbname]:
                        raise KinetaError("Unknown %s %s" % (name, id))
                    d[obj] = store[dbname][id].obj
                    store[dbname][id].update()
                    ids[obj] = id
        return d, ids

    def _do_cmd(self, dbname, db, cmd, parms, ids):
        return jsonlib.write({'r': getattr(db, cmd)(**parms)})

    def _do_select(self, dbname, db, cmd, parms, ids):
        reader = db.select(**parms)
        id = uuid.uuid4().hex
        with self._lock:
            self._readers[dbname][id] = _TimeoutObj(reader)
        return self._selectnext(reader, id, dbname, db, True)

    def _do_selectnext(self, dbname, db, cmd, parms, ids):
        return self._selectnext(parms["next"], ids["next"], dbname, db, False)

    def _do_selectclose(self, dbname, db, cmd, parms, ids):
        reader = parms["next"]
        with reader._threadlock:
            reader.close()
        with self._lock:
            if ids["next"] in self._readers[dbname]:
                del self._readers[dbname][ids["next"]]
        return '{"r":null}'

    def _selectnext(self, reader, id, dbname, db, sendfields):
        ret = StringIO.StringIO()
        ret.write("[")
        first = True
        cnt = 0

        with reader._threadlock:
            remove = False
            try:
                try:
                    while cnt < db.limits.server_row_page_bytes:
                        row = reader.next()
                        if first:
                            first = False
                        else:
                            ret.write(",")
                        s = jsonlib.write(row)
                        cnt += len(s)
                        ret.write(s)
                except StopIteration:
                    remove = True
            except: #pragma: no cover
                remove = True
                raise
            finally:
                if remove:
                    reader.close()
                    with self._lock:
                        if id in self._readers[dbname]:
                            del self._readers[dbname][id]
                        id = None

        ret.write("]")

        if id is None:
            idstr = "null"
        else:
            idstr = '"%s"' % str(id)
        if sendfields:
            return '{"r":[%s,%s],"next":%s}' % \
                                            (jsonlib.write(reader.field_names),
                                             ret.getvalue(), idstr)
        else:
            return '{"r":%s,"next":%s}' % (ret.getvalue(), idstr)

    def _do_begin(self, dbname, db, cmd, parms, ids):
        txn = db.begin(**parms)
        id = uuid.uuid4().hex
        with self._lock:
            self._txns[dbname][id] = _TimeoutObj(txn)
        return '{"r":"%s"}' % id

    def _do_commit_rollback(self, dbname, db, cmd, parms, ids):
        try:
            getattr(parms["txn"], cmd)()
        finally:
            with self._lock:
                if ids["txn"] in self._txns[dbname]:
                    del self._txns[dbname][ids["txn"]]
        return '{"r":null}'

    def _obj_timeout(self):
        with self._lock:
            now = time.time()
            for member in (self._txns, self._readers):
                for dbname, store in member.iteritems():
                    limit = self.databases[dbname].limits.server_obj_timeout
                    for id in store.keys():
                        if store[id].check(now, limit):
                            try:
                                store[id].obj.rollback()
                            except: #pragma: no cover
                                pass
                            del store[id]


class _TimeoutObj(object):

    def __init__(self, obj):
        self.obj = obj
        self.update()

    def check(self, now, limit):
        return now - self._ts > limit

    def update(self):
        self._ts = time.time()
